Release 10.1A: OpenEdge Development:
Progress Dynamics Advanced Development


Organization of the manager code

The managers are written to allow a single body of code to be compiled for use on both the server and the client, with the portions requiring database access segregated to the server. For example, when using the Profile Manager, remember that a basic principle of the managers is that there is a server version of the manager that handles all Repository database access, and a client version that other procedures in the client communicate with. The code that is common to both client and server versions is placed in an include file whose name is of the form afxxxmngrp.i, where xxx represents a three-letter abbreviation of the manager name, such as:

These include files are in the af/app directory. The af directory tree is where code related to supporting the framework itself is located (‘af’ stands for ‘application framework’). The app subdirectory is where code goes that runs on the AppServer in a distributed environment.

Two other procedures include this file:

These two preprocessors, client-side and server-side, allow the procedures in the common include file to separate out blocks of code where needed that should be compiled only on the server side of the manager or only on the client side, as we’ll see in a moment.

The code that must be separated out is largely code that accesses the Repository database. This code must be executed only on the server. Where the client needs access to the same data, it must run the same code on the AppServer.

The most efficient way to structure the manager on the server is to have all of the code, including the procedures that access the database, together in one persistent procedure where it can all be prestarted and therefore running whenever it is needed. On the other hand, when the client code must access the same database procedures, the most efficient way to do that is to make a single call to an external procedure (Progress .r file) on the server, which takes INPUT parameters and returns whatever the result is as OUTPUT parameters. Making a single call like this to a stateless AppServer session does not bind the client to the server, and gets the results in a single AppServer call. By contrast, running an internal procedure inside a larger .r file requires making a call to establish the connection and run the .r file as a persistent procedure, then a separate call to run an internal procedure entry point inside it, and then another call to delete the server procedure. The client is bound to the AppServer session for the duration of this process.

To improve functionality, the managers use a technique of isolating database access code in external .p files to be run from the client, and then including those in a version of the same code that runs on the server.

The common include file, afpromngrp.i, has code in its main block that defines an internal procedure with the same name as each external procedure that contains server-side-only code. Only if the server-side preprocessor is defined, these definitions are compiled into the procedure that includes the common code, as shown:

&IF DEFINED(server-side) <> 0 &THEN 
  PROCEDURE afbldclicp:         {af/app/afbldclicp.p}     END PROCEDURE. 
  PROCEDURE afchkpdexp:         {af/app/afchkpdexp.p}     END PROCEDURE. 
  PROCEDURE afgetpdatp:         {af/app/afgetpdatp.p}     END PROCEDURE. 
  PROCEDURE afsetpdatp:         {af/app/afsetpdatp.p}     END PROCEDURE. 
  PROCEDURE afupdcadbp:         {af/app/afupdcadbp.p}     END PROCEDURE. 
  PROCEDURE afdelsprop:         {af/app/afdelsprop.p}     END PROCEDURE. 
&ENDIF 

In this way the version of the manager that runs on the server is a single large persistent procedure that incorporates all the database access code as well as the common code. This is an efficient way to operate because the managers are pre-started and left running for the duration of the AppServer session. Note that this version of the manager is also the one that runs if you are running locally, without an AppServer.

Each of the .p files is also compiled as a separate unit, so that its code can be run from a client session. Because each of these must be a simple procedure call, not a call to an internal procedure inside some larger file, all the executable code for these data access procedures must be in the main block, the part of the file that is executed when the procedure runs, as shown in Figure 6–2.

Figure 6–2: Executable code in main block

From a client session, the data access procedures are run as individual .r files on the AppServer, as shown in Figure 6–3.

Figure 6–3: Data access file

Within the server session itself, or when there is no separate AppServer, the data access code is compiled right into the manager itself.

For example, here is an excerpt of the code in the main block of the first support procedure for the Profile Manager, afbldclicp.p, which builds the temp-table cache of profile values for a user and returns it to the session where that user is running:

/* ***************************  Main Block  *************************** */ 
  DEFINE INPUT PARAMETER  pcProfileTypeCodes            AS CHARACTER  NO-UNDO. 
  DEFINE OUTPUT PARAMETER TABLE FOR ttProfileData.  
/* loop around profile codes for profie type */ 
    FOR EACH gsc_profile_code NO-LOCK 
       WHERE gsc_profile_code.profile_type_obj =   
         gsc_profile_type.profile_type_obj: 
      FOR EACH gsm_profile_data NO-LOCK 
         WHERE gsm_profile_data.USER_obj = dUserObj 
           AND gsm_profile_data.profile_type_obj =  
             gsc_profile_code.profile_type_obj 
           AND gsm_profile_data.profile_code_obj =  
             gsc_profile_code.profile_code_obj: 
        CREATE ttProfileData. 
        BUFFER-COPY gsm_profile_data TO ttProfileData 
             ASSIGN cProfileTypeCode = gsc_profile_type.profile_type_code 
                    cProfileCode = gsc_profile_code.profile_code 
                    cAction = "NON":U. 
      END.  /* each profile data */ 

Note that the procedure can take whatever input and output parameters it needs. This one takes a list of profile type codes as input and returns the resulting temp-table of all values of those types for the current user. This is then kept on the client to be used during the session. In some cases, you might find the parameters in the Definitions section of the procedure rather than in the main block. This is just an organizational choice and doesn’t affect how the procedure compiles.

Because this is code from one of the managers itself, it makes direct references to the Repository database tables. In code that you write in your application, including custom managers you create, you should use the available API for the manager, along with general-purpose routines, such as getEntityDescription, to get data from the Repository without direct reference to table and field names. The APIs are supported and kept compatible in the future; the specifics of the underlying table structure might be subject to change in future releases of the product.

Because this code is loading a temp-table of data for the client, it should not be run on the remote part of a distributed manager, but only in the client part of a distributed manager, or in a manager that is being run without AppServer. Thus, if you look at the main block of the common code include file, you see a reference that is found frequently in the manager code, as shown:

IF NOT (SESSION:REMOTE OR SESSION:CLIENT-TYPE = "WEBSPEED":U) THEN 
  RUN buildClientCache(INPUT "":U). /* load temp-table on client */ 

Checking the SESSION object tells the code whether it has been started on an AppServer session or WebSpeed agent. If this is not the case, then the code runs the internal procedure buildClientCache. This code is in the main block of the include file, so it is run as soon as the manager is first executed on session startup.

Looking next at buildClientCache itself, you can see an example of how the internal versus external partitioning is used in the code, as shown:

PROCEDURE buildClientCache: 
DEFINE INPUT PARAMETER  pcProfileTypeCodes            AS CHARACTER  NO-UNDO. 
IF NOT (SESSION:REMOTE OR SESSION:CLIENT-TYPE = "WEBSPEED":U) THEN 
DO: 
  EMPTY TEMP-TABLE ttProfileData. 
  &IF DEFINED(server-side) <> 0 &THEN 
    RUN afbldclicp (INPUT pcProfileTypeCodes, 
                    OUTPUT TABLE ttProfileData).   
  &ELSE 
    RUN af/app/afbldclicp.p ON gshAstraAppserver (INPUT pcProfileTypeCodes, 
                                                  OUTPUT TABLE ttProfileData). 
  &ENDIF 
END. 
END PROCEDURE. 

First it checks the REMOTE parameter as before. Then it starts by emptying the Profile Data temp-table in case there is any leftover data in it. This could be the case if the session is restarted.

Next comes the code block of interest. What it says, in effect, is if this is the server-side version of the manager, compile in a RUN statement to run the cache-loading procedure afbldclicp as an internal procedure within the manager. Otherwise compile in a statement to run it as an external procedure on the default AppServer handle.

This is not a coding style to follow in new code that you write, because new features in the 4GL allow you to achieve the same flexibility without this structure. But within the existing managers, it works effectively.


Copyright © 2005 Progress Software Corporation
www.progress.com
Voice: (781) 280-4000
Fax: (781) 280-4095